/*************************************************************************
 * Copyright 2008 David Almilli
 *  
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 *  
 *     http://www.apache.org/licenses/LICENSE-2.0 
 *      
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License.
 **************************************************************************/
package com.tivo.hme.bananas;

import static com.tivo.hme.bananas.IBananasPlus.*;

import com.tivo.hme.bananas.BSkinPlus.Element;
import com.tivo.hme.bananas.layout.Layout;
import com.tivo.hme.sdk.HmeEvent;
import com.tivo.hme.sdk.Resource;
import java.awt.Point;
import java.awt.Rectangle;
import java.util.HashMap;
import java.util.Map;

public class BKeyboardPlus extends BViewPlus implements PlusView {
    private static final int EVT_KBD_VALUE = HmeEvent.EVT_RESERVED + 20;

    public static final String LOWERCASE = "lowercase";
    public static final String UPPERCASE = "uppercase";
    public static final String SYMBOL = "symbol";
    public static final String EMAIL_LOWERCASE = "email-lowercase";
    public static final String EMAIL_UPPERCASE = "email-uppercase";
    public static final String EMAIL_SYMBOL = "email-symbol";
    public static final String URL_LOWERCASE = "url-lowercase";
    public static final String URL_UPPERCASE = "url-uppercase";
    public static final String URL_SYMBOL = "url-symbol";

    public static final String PLAIN_KEYBOARD = LOWERCASE;
    public static final String EMAIL_KEYBOARD = EMAIL_LOWERCASE;
    public static final String URL_KEYBOARD = URL_LOWERCASE;
    public static final String TALL_KEYBOARD = "tall";

    public static final int INPUT_WIDTH_SAME_AS_WIDGET = -1;
    
    private boolean disposeResourceOnRemove;

    /**
     * focus manager associated with keyboard
     */
    protected BFocusMgr focusMgr = null;

    protected boolean highlightVis = false;

    /**
     * background view onto which the keys will be placed.
     */
    protected BView kbBgImage;

    protected BView kbBgImage_top;

    protected BView kbBgImage_middle;

    protected BView kbBgImage_bottom;

    protected BView dataField;

    protected BView dataFieldLeft;

    protected BView dataFieldMiddle;

    protected BView dataFieldRight;

    protected BView tipsView;

    protected BHighlightsPlus keyboardHighlights;

    protected BHighlightsPlus dataInputHighlights;

    /**
     * default selected cell
     */
    protected CellView defaultCell;

    /**
     * currently focused cell
     */
    protected CellView focused;

    /**
     * The list of all cell views
     */
    protected CellView[][] gridViews;

    /**
     * speed in which highlight moves
     */
    protected String animation;

    /**
     * The datafield in which the current value will be displayed
     */
    protected DisplayText textField;

    protected Keyboard activeKeyboard;

    protected Map<String, Keyboard> keyboardStateMap;

    /**
     * current value of the keyboard
     */
    protected String value = "";

    /**
     * current value of the keyboard
     */
    protected String undoValue = null;

    public BKeyboardPlus(BView parent, int x, int y, String keyboardName) {
        this(parent, x, y, new SkinKeyboard((BSkinPlus) parent.getBApp().getSkin(), keyboardName));
    }

    public BKeyboardPlus(BView parent, int x, int y, String keyboardName, boolean showTips) {
        this(parent, x, y, new SkinKeyboard((BSkinPlus) parent.getBApp().getSkin(), keyboardName),
                showTips);
    }

    public BKeyboardPlus(BView parent, int x, int y, Keyboard keyboard) {
        this(parent, x, y, keyboard, INPUT_WIDTH_SAME_AS_WIDGET, true);
    }

    public BKeyboardPlus(BView parent, int x, int y, Keyboard keyboard, boolean showTips) {
        this(parent, x, y, keyboard, INPUT_WIDTH_SAME_AS_WIDGET, showTips);
    }

    public BKeyboardPlus(BView parent, int x, int y, Keyboard keyboard, int textEntryWidth,
            boolean showTips) {
        this(parent, ViewUtils.getBounds(parent, x, y, getDefaultWidth(keyboard, textEntryWidth,
                showTips), keyboard.getImageHeight()), keyboard, textEntryWidth, showTips);
    }

    public BKeyboardPlus(BView parent, Layout layout, String keyboardName) {
        this(parent, layout,
                new SkinKeyboard((BSkinPlus) parent.getBApp().getSkin(), keyboardName), true);
    }

    public BKeyboardPlus(BView parent, Layout layout, String keyboardName, boolean showTips) {
        this(parent, layout,
                new SkinKeyboard((BSkinPlus) parent.getBApp().getSkin(), keyboardName), showTips);
    }

    public BKeyboardPlus(BView parent, Layout layout, Keyboard keyboard) {
        this(parent, layout, keyboard, INPUT_WIDTH_SAME_AS_WIDGET, true);
    }

    public BKeyboardPlus(BView parent, Layout layout, Keyboard keyboard, boolean showTips) {
        this(parent, layout, keyboard, INPUT_WIDTH_SAME_AS_WIDGET, showTips);
    }

    public BKeyboardPlus(BView parent, Layout layout, Keyboard keyboard, int textEntryWidth,
            boolean showTips) {
        this(parent, layout.getManager().defaultSize(layout,
                getDefaultWidth(keyboard, textEntryWidth, showTips), keyboard.getImageHeight())
                .getBounds(), keyboard, textEntryWidth, showTips);
    }

    protected BKeyboardPlus(BView parent, Rectangle bounds, Keyboard keyboard, int textEntryWidth,
            boolean showTips) {
        super(parent, bounds.x, bounds.y, bounds.width, bounds.height);

        keyboardStateMap = new HashMap<String, Keyboard>();
        keyboardStateMap.put(keyboard.getName(), keyboard);

        focusMgr = createFocusManager();
        if (keyboard == null) {
            throw new IllegalArgumentException("Keyboard can not be null");
        }

        if (textEntryWidth == INPUT_WIDTH_SAME_AS_WIDGET) {
            textEntryWidth = this.getWidth();
        }

        int dataCap = keyboard.getDataInputCap();
        int dataPadV = keyboard.getDataInputPadV();
        int dataWidth = keyboard.getDataInputImageWidth();
        int dataHeight = keyboard.getDataInputImageHeight();
        Object dataResource = keyboard.getDataInputImageResource();
        dataField = new BView(this, 0, 0, textEntryWidth, dataHeight);

        dataFieldLeft = new BView(dataField, 0, 0, dataCap, dataHeight);
        dataFieldLeft.setResource(dataResource, RSRC_HALIGN_LEFT);

        dataFieldMiddle = new BTilesPlus(dataField, dataCap, 0, textEntryWidth - 2 * dataCap,
                dataHeight, dataWidth - 2 * dataCap, dataHeight);
        dataFieldMiddle.setResource(dataResource, RSRC_HALIGN_CENTER);

        dataFieldRight = new BView(dataField, textEntryWidth - dataCap, 0, dataCap, dataHeight);
        dataFieldRight.setResource(dataResource, RSRC_HALIGN_RIGHT);

        textField = new DisplayText(dataField, dataCap, 1, dataField.getWidth() - 2 * dataCap,
                dataHeight, keyboard.getFontResource(), keyboard.getColorResource());
        textField.setFlags(RSRC_HALIGN_LEFT);

        int spacing = dataHeight + dataPadV;
        kbBgImage = new BView(this, 0, spacing, keyboard.getImageWidth(), keyboard.getImageHeight()
                - spacing);
        int t = 0;
        kbBgImage_top = new BView(kbBgImage, 0, t, keyboard.getImageWidth(), keyboard
                .getTopImageHeight());
        t += keyboard.getTopImageHeight();
        kbBgImage_middle = new BView(kbBgImage, 0, t, keyboard.getImageWidth(), keyboard
                .getMiddleImageHeight());
        t += keyboard.getMiddleImageHeight();
        kbBgImage_bottom = new BView(kbBgImage, 0, t, keyboard.getImageWidth(), keyboard
                .getBottomImageHeight());

        if (showTips) {
            int tipsPadH = keyboard.getTipsPadH();
            int tipsPadV = keyboard.getTipsPadV();
            tipsView = new BView(this, keyboard.getImageWidth() + tipsPadH, spacing + tipsPadV,
                    keyboard.getTipsImageWidth(), keyboard.getTipsImageHeight());
            tipsView.setResource(keyboard.getTipsImageResource());
        }

        replaceKeyboard(keyboard);

        BSkin.Element e = getBApp().getSkin().get(H_KEYBOARD);
        int padH = e.getInt(P_PAD_H, 5);
        int padV = e.getInt(P_PAD_V, 10);
        BHighlight h = new BHighlight(e.getName(), null, -padH, -padV);
        h.setAbove(true);
        h.setStretchWidth(focused.getWidth() + 2 * padH);
        getKeyboardHighlights().set(h);

        refreshHighlights(animation);
    }
    
    protected BFocusMgrPlus createFocusManager() {
        return new BFocusMgrPlus(getBApp());
    }

    @Override
    public void remove(Resource animation) {
        PlusSupport.viewRemoveNotify(this);
        super.remove(animation);
    }
    
    protected void viewRemoveNotify() {
        if (getDisposeResourceOnRemove()) {
            Resource resource = getResource();
            if (resource != null) {
                //remove the resource
                resource.remove();
            }
        }
    }

    public boolean getDisposeResourceOnRemove() {
        return disposeResourceOnRemove;
    }

    public void setDisposeResourceOnRemove(boolean disposeResourceOnRemove) {
        this.disposeResourceOnRemove = disposeResourceOnRemove;
    }
    
    public Rectangle getScreenFocusBounds() {
        BPoint s = toScreen();
        int width;
        if (activeKeyboard != null) {
            width = activeKeyboard.getImageWidth();
        } else {
            width = getWidth();
        }
        return new Rectangle(s.x, s.y, width, getHeight());
    }

    private static int getDefaultWidth(Keyboard keyboard, int dataFieldWidth, boolean showTips) {
        int width = keyboard.getImageWidth();
        if (showTips) {
            width += keyboard.getTipsImageWidth() + keyboard.getTipsPadH();
        }
        return Math.max(dataFieldWidth, width);
    }

    /**
     * Set a new key grid, remove the current views associated with the old grid and replace them
     * with new grid.
     * 
     * This will also attempt to keep the highlight in the same place, it first looks for a key with
     * the same value of the old highlight and failing that tries to find a key in the location.
     * 
     * This is a thread-safe public entry point.
     * 
     * @param keyboard
     *            The grid which will be displayed
     */
    protected void replaceKeyboard(Keyboard keyboard) {
        synchronized (lock) {
            activeKeyboard = keyboard;

            // get the string of the currently focused item
            // after replacing the grid we will look for this and
            // reset the focus

            Rectangle oldFocusBounds = null;
            if (focused != null) {
                oldFocusBounds = focused.getBounds();
            }
            focused = null;

            setPainting(false);

            // set up views into which we will place our images
            kbBgImage.setSize(keyboard.getImageWidth(), keyboard.getImageHeight());
            int t = 0;
            kbBgImage_top.setBounds(0, t, keyboard.getImageWidth(), keyboard.getTopImageHeight());
            t += keyboard.getTopImageHeight();
            kbBgImage_middle.setBounds(0, t, keyboard.getImageWidth(), keyboard
                    .getMiddleImageHeight());
            t += keyboard.getMiddleImageHeight();
            kbBgImage_bottom.setBounds(0, t, keyboard.getImageWidth(), keyboard
                    .getBottomImageHeight());

            kbBgImage_top.setResource(keyboard.getTopImageResource());
            kbBgImage_middle.setResource(keyboard.getMiddleImageResource());
            kbBgImage_bottom.setResource(keyboard.getBottomImageResource());

            // remove current grid

            boolean tryAgain = true;
            while (tryAgain) {
                int numFound = 0;
                for (int i = 0; i < kbBgImage.getChildCount(); i++) {
                    if (kbBgImage.getChild(i) instanceof CellView) {
                        kbBgImage.getChild(i).setVisible(false);
                        kbBgImage.getChild(i).remove();

                        ++numFound;
                        break;
                    }
                }
                if (numFound == 0) {
                    tryAgain = false;
                }
            }

            // create new grid

            Cell[][] newgrid = keyboard.getCells();
            // find the longest line
            int longest = 0;
            for (int i = 0; i < newgrid.length; i++) {
                if (newgrid[i].length > longest) {
                    longest = newgrid[i].length;
                }
            }
            // make a grid of the views
            gridViews = new CellView[newgrid.length][longest];

            for (int i = 0; i < newgrid.length; i++) {
                int view_x = 10;
                for (int j = 0; j < newgrid[i].length; j++) {
                    if (newgrid[i][j] != null && newgrid[i][j].type != Cell.CELL_TYPE_EMPTY) {
                        int w = newgrid[i][j].width;

                        gridViews[i][j] = new CellView(kbBgImage, newgrid[i][j]);
                        view_x += w;
                    }
                }
            }

            Point defaultPoint = keyboard.getDefaultCell();
            defaultCell = gridViews[defaultPoint.x][defaultPoint.y];

            //
            // if we didn't set the focus by string, set it by location
            // 
            if (oldFocusBounds != null) {
                int centerX = oldFocusBounds.x + (oldFocusBounds.width / 2);
                int centerY = oldFocusBounds.y + (oldFocusBounds.height / 2);
                for (int i = 0; i < kbBgImage.getChildCount(); i++) {
                    if (kbBgImage.getChild(i) instanceof CellView) {
                        if (kbBgImage.getChild(i).getBounds().contains(centerX, centerY)) {
                            focused = (CellView) kbBgImage.getChild(i);
                            break;
                        }
                    }
                }

            }

            // if all else fails just set the focus to the default item
            if (focused == null) {
                focusDefaultCell();
            }

            setPainting(true);
            flush();

            refreshHighlights(animation);
        } // synch
    } // replaceKeyboard()

    /**
     * move the hilite focus to the currently focused key
     * 
     * This is a thread-safe public entry point.
     */
    protected void refreshHighlights(String anim) {
        synchronized (lock) {
            if (anim != null) {
                Resource animation = getResource(anim);
                getKeyboardHighlights().refresh(animation);
                getDataInputHighlights().refresh(animation);
            } else {
                getKeyboardHighlights().refresh();
                getDataInputHighlights().refresh();
            }
        }
    }

    public boolean handleEvent(HmeEvent event) {
        synchronized (lock) {

            if (event instanceof KeyboardEvent) {
                textField.update(((KeyboardEvent) event).getValue());
            }
            return super.handleEvent(event);

        }
    }

    /**
     * Upon gaining the focus we want to make sure the highlight is visible, then hide it when we
     * lose focus.
     * 
     * <p>
     * This is a thread-safe public entry point.
     * </p>
     */
    public boolean handleFocus(boolean gotFocus, BView gained, BView lost) {
        synchronized (lock) {
            if (gotFocus && gained == this) {
                getScreen().setFocus(focused);
                highlightVis = true;
                refreshHighlights(animation);
            } else if (gotFocus == false && lost == this) {
                highlightVis = false;
                refreshHighlights(animation);
            } else {
                highlightVis = gained instanceof CellView;
                refreshHighlights(animation);
            }

            return super.handleFocus(gotFocus, gained, lost);
        } // synch
    }

    /**
     * repeat keypresses are treated as regular jey presses
     * 
     * <p>
     * This is a thread-safe public entry point.
     * </p>
     */
    public boolean handleKeyRepeat(int code, long rawcode) {
        // synchronization handled by method we are calling
        return handleKeyPress(code, rawcode);
    }

    /**
     * Use the keys to navigate and also allow the user to press the number geys to enter numbers
     * 
     * <p>
     * This is a thread-safe public entry point.
     * </p>
     */
    public boolean handleKeyPress(int code, long rawcode) {
        synchronized (lock) {
            switch (code) {
            case KEY_RIGHT:
                BView newFocus = focusMgr.followArrow(focused, KEY_RIGHT);
                if (newFocus instanceof CellView) {
                    focused = (CellView) newFocus;
                    getScreen().setFocus(focused);
                    BHighlight h = getKeyboardHighlights().get(H_KEYBOARD);
                    BSkin.Element e = getBApp().getSkin().get(H_KEYBOARD);
                    int padH = e.getInt(P_PAD_H, 10);
                    h.setStretchWidth(focused.getWidth() + 2 * padH);
                    getBApp().play("updown.snd");
                    refreshHighlights(animation);
                } else {
                    getParent().postEvent(new BEvent.Action(this, "right"));
                }
                return true;
            case KEY_LEFT:
                newFocus = focusMgr.followArrow(focused, KEY_LEFT);
                if (newFocus instanceof CellView) {
                    focused = (CellView) newFocus;
                    getScreen().setFocus(focused);
                    BHighlight h = getKeyboardHighlights().get(H_KEYBOARD);
                    BSkin.Element e = getBApp().getSkin().get(H_KEYBOARD);
                    int padH = e.getInt(P_PAD_H, 10);
                    h.setStretchWidth(focused.getWidth() + 2 * padH);

                    getBApp().play("updown.snd");

                    refreshHighlights(animation);
                } else {
                    getParent().postEvent(new BEvent.Action(this, "left"));
                }
                return true;
            case KEY_UP:
                newFocus = focusMgr.followArrow(focused, KEY_UP);
                if (newFocus instanceof CellView) {
                    focused = (CellView) newFocus;
                    getScreen().setFocus(focused);
                    BHighlight h = getKeyboardHighlights().get(H_KEYBOARD);
                    BSkin.Element e = getBApp().getSkin().get(H_KEYBOARD);
                    int padH = e.getInt(P_PAD_H, 10);
                    h.setStretchWidth(focused.getWidth() + 2 * padH);

                    getBApp().play("updown.snd");
                    refreshHighlights(animation);
                } else {
                    getParent().postEvent(new BEvent.Action(this, "up"));
                }
                return true;
            case KEY_DOWN:
                newFocus = focusMgr.followArrow(focused, KEY_DOWN);
                if (newFocus instanceof CellView) {
                    focused = (CellView) newFocus;
                    getScreen().setFocus(focused);

                    BHighlight h = getKeyboardHighlights().get(H_KEYBOARD);
                    BSkin.Element e = getBApp().getSkin().get(H_KEYBOARD);
                    int padH = e.getInt(P_PAD_H, 10);
                    h.setStretchWidth(focused.getWidth() + 2 * padH);

                    getBApp().play("updown.snd");

                    refreshHighlights(animation);
                } else {
                    getParent().postEvent(new BEvent.Action(this, "down"));
                }
                return true;
            case KEY_SELECT:

                if (focused != null) {
                    handleSelection(focused.getType(), focused.getValue());
                }
                return true;
            case KEY_THUMBSUP:
                String thumbsUpName = activeKeyboard.getThumbsUpKeyboard();
                if (thumbsUpName != null) {
                    handleSelection(Cell.CELL_TYPE_KEYBOARD, thumbsUpName);
                }
                return true;
            case KEY_THUMBSDOWN:
                String thumbsDownName = activeKeyboard.getThumbsDownKeyboard();
                if (thumbsDownName != null) {
                    handleSelection(Cell.CELL_TYPE_KEYBOARD, thumbsDownName);
                }
                return true;
            case KEY_REVERSE:
                handleSelection(Cell.CELL_TYPE_DEL, null);
                return true;
            case KEY_FORWARD:
                handleSelection(Cell.CELL_TYPE_CHAR, " ");
                return true;
            case KEY_NUM0:
                handleSelection(Cell.CELL_TYPE_CHAR, "0");
                return true;
            case KEY_NUM1:
                handleSelection(Cell.CELL_TYPE_CHAR, "1");
                return true;
            case KEY_NUM2:
                handleSelection(Cell.CELL_TYPE_CHAR, "2");
                return true;
            case KEY_NUM3:
                handleSelection(Cell.CELL_TYPE_CHAR, "3");
                return true;
            case KEY_NUM4:
                handleSelection(Cell.CELL_TYPE_CHAR, "4");
                return true;
            case KEY_NUM5:
                handleSelection(Cell.CELL_TYPE_CHAR, "5");
                return true;
            case KEY_NUM6:
                handleSelection(Cell.CELL_TYPE_CHAR, "6");
                return true;
            case KEY_NUM7:
                handleSelection(Cell.CELL_TYPE_CHAR, "7");
                return true;
            case KEY_NUM8:
                handleSelection(Cell.CELL_TYPE_CHAR, "8");
                return true;
            case KEY_NUM9:
                handleSelection(Cell.CELL_TYPE_CHAR, "9");
                return true;
            case KEY_CLEAR:
                handleSelection(Cell.CELL_TYPE_CLR, null);
                return true;
            }
            return super.handleKeyPress(code, rawcode);
        } // synch
    }

    /**
     * Set a keyboard to a keyboard state
     * 
     * a BKeyboard consists of 4 states:
     * 
     * LOWERCASE; UPPERCASE; SYMBOL; NUMCAPSLOCK;
     * 
     * each state needs a keyboard associated with it
     * 
     * If you are changing a keybord of the current state, the new keyboard will be displayed
     * immediately
     * 
     * This is a thread-safe public entry point.
     * 
     * @param state
     *            The state of the keyboard
     * @param keyboard
     *            a keyboard
     */
    public void linkKeyboardToState(String name, Keyboard keyboard) {
        synchronized (lock) {

            Keyboard existing = keyboardStateMap.get(name);
            keyboardStateMap.put(name, keyboard);

            // if we're replacing the curerent state then replace the keyboard
            if (existing == keyboard) {
                setKeyboard(name);
            }
        } // synch
    }

    public void setKeyboard(String name) {
        synchronized (lock) {
            Keyboard keyboard = keyboardStateMap.get(name);
            if (keyboard == null) {
                keyboard = new SkinKeyboard((BSkinPlus) getBApp().getSkin(), name);
                keyboardStateMap.put(name, keyboard);
            }
            if (keyboard != activeKeyboard) {
                replaceKeyboard(keyboard);
            }
        }
    }

    /**
     * Handle an item selected from the keyboard, the character selected is passed in
     * 
     * <p>
     * This is a thread-safe public entry point.
     * </p>
     */
    protected void handleSelection(int type, String value) {
        synchronized (lock) {

            //
            // change keyboard and return
            //

            switch (type) {

            case Cell.CELL_TYPE_KEYBOARD:
                setKeyboard(value);
                getBApp().play("select.snd");
                return;

            case Cell.CELL_TYPE_DEL:
                if (this.value.length() > 0) {
                    this.value = this.value.substring(0, this.value.length() - 1);
                    notifyListeners();
                    getBApp().play("select.snd");
                } else {
                    getBApp().play("bonk.snd");
                }
                return;
            case Cell.CELL_TYPE_UNDO:
                setValue(undoValue);
                undoValue = null;
                setUndoState(false);
                getBApp().play("select.snd");
                return;
            case Cell.CELL_TYPE_CLR:
                if (this.value.length() > 0) {
                    setUndoState(true);
                    undoValue = getValue();
                    this.value = "";
                    notifyListeners();
                    getBApp().play("select.snd");
                } else {
                    getBApp().play("bonk.snd");
                }
                return;
            case Cell.CELL_TYPE_CHAR:
                setValue(this.value += value);
                getBApp().play("select.snd");
                break;
            }
        } // synch
    }

    /**
     * Tell all the clients that the text associated with the keyboard has changed
     * 
     * <p>
     * This is a thread-safe public entry point.
     * </p>
     */
    protected void notifyListeners() {
        synchronized (lock) {
            if (getValue() != null) {
                postEvent(new KeyboardEvent(EVT_KBD_VALUE, this.getID(), getValue()));
            }
        } // synch
    }

    /**
     * Get the value of the widget, a string representing what has been entered
     * 
     * This is a thread-safe public entry point.
     * 
     * @return the string that has been typed in
     */
    public String getValue() {
        // getter methods generally don't need synchronization
        return value;
    }

    /**
     * Set the value of the widget, all clients will be notified imediately of the change
     * 
     * @param newWord
     *            New valuee of the widget
     */
    public void setValue(String newWord) {
        synchronized (lock) {
            if (newWord == null) {
                newWord = "";
            }
            if (undoValue != null) {
                setUndoState(false);
            }
            this.value = newWord;
            notifyListeners();
        } // synchronized
    }

    /**
     * 
     * This is a thread-safe public entry point.
     * 
     * @param undo
     */
    protected void setUndoState(boolean undo) {
        synchronized (lock) {
            Keyboard kb = activeKeyboard;
            if (undo == true) {
                Resource newImage = kb.getTopUndoImageResource();
                if (newImage == null) {
                    return;

                }
                // scan first occurrence for CLR, replace with UNDO
                CellView[][] cells = this.gridViews;
                for (int i = 0; i < cells.length; i++) {
                    CellView[] row = cells[i];
                    for (int j = 0; j < row.length; j++) {
                        if (row[j] != null && row[j].getType() == Cell.CELL_TYPE_CLR) {
                            kbBgImage_top.setResource(newImage);
                            row[j].setType(Cell.CELL_TYPE_UNDO);
                            break;
                        }
                    }
                }
            } else {
                Resource newImage = kb.getTopImageResource();
                // scan first occurrence of an overridden type and reset
                CellView[][] cells = this.gridViews;
                for (int i = 0; i < cells.length; i++) {
                    CellView[] row = cells[i];
                    for (int j = 0; j < row.length; j++) {
                        if (row[j] != null && row[j].isTypeOverridden()) {
                            kbBgImage_top.setResource(newImage);
                            row[j].resetType();
                            break;
                        }
                    }
                }
            }
        } // synch
    }

    /**
     * set focus on the default cell
     */
    public void focusDefaultCell() {
        synchronized (lock) {
            if (defaultCell == null) {
                // this should never happen;
                return;
            }

            focused = defaultCell;
            getScreen().setFocus(focused);
            BHighlights keyboardHighlights = getKeyboardHighlights();
            if (keyboardHighlights != null) {
                BHighlight h = keyboardHighlights.get(H_KEYBOARD);
                if (h != null) {
                    BSkin.Element e = getBApp().getSkin().get(H_KEYBOARD);
                    int padH = e.getInt(P_PAD_H, 10);
                    h.setStretchWidth(focused.getWidth() + 2 * padH);
                }
            }
            refreshHighlights(animation);
        }
    }

    public Keyboard getActiveKeyboard() {
        return activeKeyboard;
    }

    public BHighlightsPlus getKeyboardHighlights() {
        if (keyboardHighlights == null) {
            keyboardHighlights = createKeyboardHighlights();
        }
        return keyboardHighlights;
    }

    public void setKeyboardHighlights(BHighlightsPlus keyboardHighlights) {
        synchronized (lock) {
            this.keyboardHighlights = keyboardHighlights;
            refreshHighlights(animation);
        }
    }

    protected BHighlightsPlus createKeyboardHighlights() {
        return new BHighlightsPlus(new HighlightsLayout());
    }

    public BHighlightsPlus getDataInputHighlights() {
        if (dataInputHighlights == null) {
            dataInputHighlights = createDataInputHighlights();
        }
        return dataInputHighlights;
    }

    public void setDataInputHighlights(BHighlightsPlus dataInputHighlights) {
        synchronized (lock) {
            this.dataInputHighlights = dataInputHighlights;
            refreshHighlights(animation);
        }
    }

    protected BHighlightsPlus createDataInputHighlights() {
        return new BHighlightsPlus(new InputLayout());
    }

    public class InputLayout implements IHighlightsLayout {

        public BScreen getScreen() {
            return BKeyboardPlus.this.getScreen();
        }

        public BRect getHighlightBounds() {
            Rectangle bounds = dataField.getBounds();
            BRect rect = new BRect(bounds.x, bounds.y, bounds.width, bounds.height);
            return toScreenBounds(rect);
        }

        public boolean getHighlightIsVisible(int visible) {
            return dataField.getHighlightIsVisible(visible);
        }
    }

    public class HighlightsLayout implements IHighlightsLayout {

        public BScreen getScreen() {
            return BKeyboardPlus.this.getScreen();
        }

        public BRect getHighlightBounds() {
            // synchronization handled by method we are calling
            return getFocusBounds();
        }

        /**
         * <p>
         * This is a thread-safe public entry point.
         * </p>
         */
        public BRect getFocusBounds() {
            synchronized (lock) {

                if (focused != null) {
                    BRect rect = focused.getHighlightBounds();
                    return rect;
                }

                return toScreenBounds(new BRect(0, 0, getWidth(),
                        activeKeyboard.getCells()[0][0].height));
            } // synch
        }

        public boolean getHighlightIsVisible(int visible) {
            return highlightVis;
        }
    }

    public static class DisplayText extends BTextPlus {

        protected HmeEvent.FontInfo fi;

        protected int curAlignment = RSRC_HALIGN_LEFT;

        /**
         * Constructor
         * 
         * <p>
         * This is a thread-safe public entry point.
         * </p>
         */
        public DisplayText(BView parent, int x, int y, int width, int height, Resource font,
                Resource color) {
            super(parent, x, y, width, height);
            synchronized (lock) {
                setFont(font);
                setColor(color);
                update("");

                font.addHandler(this);
            }
        }

        /*
         * <p>This is a thread-safe public entry point.</p>
         * 
         * @see com.tivo.hme.bananas.IKeyboardClient#update(java.lang.String)
         */
        public void update(String word) {
            synchronized (lock) {
                setPainting(false);
                String newWord = word + "_";
                if (fi != null) {
                    int i = fi.measureTextWidth(newWord);
                    if (i > this.getWidth()) {
                        if (curAlignment != RSRC_HALIGN_RIGHT) {
                            this.setFlags(RSRC_HALIGN_RIGHT);
                            curAlignment = RSRC_HALIGN_RIGHT;
                        }
                    } else {
                        if (curAlignment != RSRC_HALIGN_LEFT) {
                            this.setFlags(RSRC_HALIGN_LEFT);
                            curAlignment = RSRC_HALIGN_LEFT;
                        }
                    }
                    setValue(newWord);
                } else {
                    setValue(newWord);
                }
                setPainting(true);
            }
        } // update()

        /**
         * <p>
         * This is a thread-safe public entry point.
         * </p>
         */
        public boolean handleEvent(HmeEvent event) {
            synchronized (lock) {

                switch (event.getOpCode()) {
                case EVT_FONT_INFO:
                    fi = (HmeEvent.FontInfo) event;
                    return true;
                }

                return false;

            } // synch
        } // handleEvent()
    }

    public static class CellView extends BView {
        protected Cell cell;

        private int type;

        /**
         * Constructor
         */
        public CellView(BView parent, Cell cell) {
            super(parent, cell.x, cell.y, cell.width, cell.height);
            this.cell = cell;
            this.type = cell.type;
            setFocusable(true);
        }

        public boolean isTypeOverridden() {
            return type != cell.type;
        }

        public void resetType() {
            setType(cell.type);
        }

        public int getType() {
            return type;
        }

        public void setType(int type) {
            this.type = type;
        }

        /**
         * Never returns null.
         */
        public String getValue() {
            synchronized (lock) {
                switch (type) {
                case Cell.CELL_TYPE_KEYBOARD:
                    return cell.value;
                case Cell.CELL_TYPE_CLR:
                    return "CLR";
                case Cell.CELL_TYPE_EMPTY:
                    return "EMPTY";
                case Cell.CELL_TYPE_UNDO:
                    return "UNDO";
                case Cell.CELL_TYPE_CHAR:
                    if (cell.value == null) {
                        return "";
                    }
                    return cell.value;
                }
                return "";
            }
        }

        public String toString() {
            return "" + cell.value;
        }
    }

    public static class Cell {
        public static final int CELL_TYPE_DEL = 1;

        public static final int CELL_TYPE_CLR = 2;

        public static final int CELL_TYPE_KEYBOARD = 3;

        public static final int CELL_TYPE_UNDO = 4;

        public static final int CELL_TYPE_CHAR = 7;

        public static final int CELL_TYPE_EMPTY = 8;

        int type = CELL_TYPE_EMPTY;

        String value = null;

        int x;

        int y;

        int width;

        int height;

        public Cell(int type, String value, int x, int y, int width, int height) {
            this.type = type;
            this.value = value;
            this.x = x;
            this.y = y;
            this.width = width;
            this.height = height;
        }
    }

    public static class KeyboardEvent extends HmeEvent {
        private String value;

        /**
         * This constructor does not access class-static variables, so it does not need to be
         * synchronized.
         * 
         * @param opcode
         * @param id
         */
        public KeyboardEvent(int opcode, int id, String value) {
            super(opcode, id);
            this.value = value;
        }

        public String getValue() {
            return value;
        }

        public void setValue(String value) {
            this.value = value;
        }
    }

    public static interface Keyboard {
        public String getName();

        public Point getDefaultCell();

        public int getImageHeight();

        public int getImageWidth();

        public Cell[][] getCells();

        public Resource getBottomImageResource();

        public int getBottomImageHeight();

        public Resource getMiddleImageResource();

        public int getMiddleImageHeight();

        public Resource getTopImageResource();

        public int getTopImageHeight();

        public Resource getTopUndoImageResource();

        public Resource getDataInputImageResource();

        public int getDataInputImageHeight();

        public int getDataInputImageWidth();

        public int getDataInputCap();

        public int getDataInputPadH();

        public int getDataInputPadV();

        public Resource getTipsImageResource();

        public int getTipsImageWidth();

        public int getTipsImageHeight();

        public int getTipsPadH();

        public int getTipsPadV();

        public Resource getFontResource();

        public Resource getColorResource();

        public String getThumbsUpKeyboard();

        public String getThumbsDownKeyboard();
    }

    public static class SkinKeyboard implements Keyboard {
        protected BSkinPlus skin;

        protected String name;

        protected String thumbsUpName;

        protected String thumbsDownName;

        public SkinKeyboard(BSkinPlus skin, String name) {
            this.skin = skin;
            this.name = name;
            int index = name.lastIndexOf('-');
            if (index != -1) {
                String prefix = name.substring(index + 1);
                thumbsDownName = prefix + LOWERCASE;
                thumbsUpName = prefix + UPPERCASE;
            } else {
                thumbsDownName = LOWERCASE;
                thumbsUpName = UPPERCASE;
            }

            //allow skin to override key mappings
            Element e = skin.get(name + H_KEYBOARD_TOP_IMAGE_SUFFIX);
            if (e.containsKey(P_KEYBOARD_THUMBS_UP)) {
                thumbsUpName = e.get(P_KEYBOARD_THUMBS_UP);
            }
            if (e.containsKey(P_KEYBOARD_THUMBS_DOWN)) {
                thumbsDownName = e.get(P_KEYBOARD_THUMBS_DOWN);
            }
        }

        public String getThumbsUpKeyboard() {
            return thumbsUpName;
        }

        public String getThumbsDownKeyboard() {
            return thumbsDownName;
        }

        public String getName() {
            return name;
        }

        /**
         * Get a cell that is considered the default so that upon switching to a new layout, the
         * keyboard knows where to place the highlight
         * 
         * @return The cell that should be focused initially or when the keyboard is cleared
         */
        public Point getDefaultCell() {
            return new Point(0, 0);
        }

        public int getImageHeight() {
            return getTopImageHeight() + getMiddleImageHeight() + getBottomImageHeight()
                    + getDataInputImageHeight() + getDataInputPadV();
        }

        public int getImageWidth() {
            Element e = skin.get(name + H_KEYBOARD_TOP_IMAGE_SUFFIX);
            if (e != null) {
                return e.getWidth();
            } else {
                return 0;
            }
        }

        public Cell[][] getCells() {
            Element e = skin.get(name + H_KEYBOARD_TOP_IMAGE_SUFFIX);
            if (e != null) {
                return (Cell[][]) e.getObject("cells");
            } else {
                return null;
            }
        }

        public Resource getBottomImageResource() {
            Element e = skin.get(name + H_KEYBOARD_BOTTOM_IMAGE_SUFFIX);
            if (e != null) {
                return e.getResource();
            } else {
                return null;
            }
        }

        public int getBottomImageHeight() {
            Element e = skin.get(name + H_KEYBOARD_BOTTOM_IMAGE_SUFFIX);
            if (e != null) {
                return e.getHeight();
            } else {
                return 0;
            }
        }

        public Resource getMiddleImageResource() {
            Element e = skin.get(name + H_KEYBOARD_MIDDLE_IMAGE_SUFFIX);
            if (e != null) {
                return e.getResource();
            } else {
                return null;
            }
        }

        public int getMiddleImageHeight() {
            Element e = skin.get(name + H_KEYBOARD_MIDDLE_IMAGE_SUFFIX);
            if (e != null) {
                return e.getHeight();
            } else {
                return 0;
            }
        }

        public Resource getTopImageResource() {
            Element e = skin.get(name + H_KEYBOARD_TOP_IMAGE_SUFFIX);
            if (e != null) {
                return e.getResource();
            } else {
                return null;
            }
        }

        public int getTopImageHeight() {
            Element e = skin.get(name + H_KEYBOARD_TOP_IMAGE_SUFFIX);
            if (e != null) {
                return e.getHeight();
            } else {
                return 0;
            }
        }

        public Resource getTopUndoImageResource() {
            Element e = skin.get(name + H_KEYBOARD_TOP_UNDO_IMAGE_SUFFIX);
            if (e != null) {
                return e.getResource();
            } else {
                return null;
            }
        }

        public Resource getDataInputImageResource() {
            Element e = skin.get(name + H_KEYBOARD_DATA_INPUT_IMAGE_SUFFIX);
            if (e != null) {
                return e.getResource();
            } else {
                return null;
            }
        }

        public int getDataInputImageWidth() {
            Element e = skin.get(name + H_KEYBOARD_DATA_INPUT_IMAGE_SUFFIX);
            if (e != null) {
                return e.getWidth();
            } else {
                return 0;
            }
        }

        public int getDataInputImageHeight() {
            Element e = skin.get(name + H_KEYBOARD_DATA_INPUT_IMAGE_SUFFIX);
            if (e != null) {
                return e.getHeight();
            } else {
                return 0;
            }
        }

        public int getDataInputPadH() {
            Element e = skin.get(name + H_KEYBOARD_DATA_INPUT_IMAGE_SUFFIX);
            if (e != null) {
                return e.getInt(P_PAD_H);
            } else {
                return 0;
            }
        }

        public int getDataInputCap() {
            Element e = skin.get(name + H_KEYBOARD_DATA_INPUT_IMAGE_SUFFIX);
            if (e != null) {
                return e.getInt(P_CAP);
            } else {
                return 0;
            }
        }

        public int getDataInputPadV() {
            Element e = skin.get(name + H_KEYBOARD_DATA_INPUT_IMAGE_SUFFIX);
            if (e != null) {
                return e.getInt(P_PAD_V);
            } else {
                return 0;
            }
        }

        public Resource getFontResource() {
            Element e = skin.get(name + H_KEYBOARD_FONT_SUFFIX);
            if (e != null) {
                return e.getResource();
            } else {
                return null;
            }
        }

        public Resource getColorResource() {
            Element e = skin.get(name + H_KEYBOARD_COLOR_SUFFIX);
            if (e != null) {
                return e.getResource();
            } else {
                return null;
            }
        }

        public Resource getTipsImageResource() {
            Element e = skin.get(name + H_KEYBOARD_TIPS_IMAGE_SUFFIX);
            if (e != null) {
                return e.getResource();
            } else {
                return null;
            }
        }

        public int getTipsImageWidth() {
            Element e = skin.get(name + H_KEYBOARD_TIPS_IMAGE_SUFFIX);
            if (e != null) {
                return e.getWidth();
            } else {
                return 0;
            }
        }

        public int getTipsImageHeight() {
            Element e = skin.get(name + H_KEYBOARD_TIPS_IMAGE_SUFFIX);
            if (e != null) {
                return e.getHeight();
            } else {
                return 0;
            }
        }

        public int getTipsPadH() {
            Element e = skin.get(name + H_KEYBOARD_TIPS_IMAGE_SUFFIX);
            if (e != null) {
                return e.getInt(P_PAD_H, 0);
            } else {
                return 0;
            }
        }

        public int getTipsPadV() {
            Element e = skin.get(name + H_KEYBOARD_TIPS_IMAGE_SUFFIX);
            if (e != null) {
                return e.getInt(P_PAD_V, 0);
            } else {
                return 0;
            }
        }
    }

}
